package hw;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

public class P2PConnection {
	private final ArrayList<AbstractClient<Bundle>> neighborsAccess = new ArrayList<AbstractClient<Bundle>>();
	private AbstractServer server;
	private File p2pfileToShare;
	private FileChangesBroadcasterThread fileChangesBroadcasterThread;
	private long lastModified;
	private long lastHeardOf;
	private int syncTime;
	private long lastWroteToFile;
	private final Object lock = new Object();
	public P2PConnection(final File fileToShare,final String neighbors,final int port,final int synctime) throws IOException {
		syncTime = synctime;
		if (fileToShare.length() > Integer.MAX_VALUE) {
			throw new IOException("file is too big");
		}
		lastModified = lastHeardOf = lastWroteToFile = fileToShare.lastModified();
		if ( neighbors!=null ) {
			final String[] split = neighbors.split(";");
			for (final String neighbor : split) {
				final String[] ipport = neighbor.split(":");
				final AbstractClient<Bundle> fileClient = new AbstractClient<Bundle>(ipport[0], Integer.parseInt(ipport[1])) {
					@Override
					protected void handleMessageFromServer(final Bundle msg) {
						if (!(msg instanceof Bundle)) return;
						synchronized (lock) {
							Report.report("Client connected to " + getIPPort() + " of this node Recieved " + msg + " msg.lastModified = "+ Report.format(msg.getLastModified())  + " lastModified = " + Report.format(lastModified) + " lastHeardOf = " + Report.format(lastHeardOf) + " lastWroteToFile = "+ Report.format(lastWroteToFile)  );
							handleMessege(msg,this);

						}
					}

				};
				neighborsAccess.add(fileClient);
			}
		}
		p2pfileToShare = fileToShare;
		if (syncTime>0) {
			fileChangesBroadcasterThread = new FileChangesBroadcasterThread(synctime);
			fileChangesBroadcasterThread.addWatcher(new FileChangesWatcher(fileToShare));
			fileChangesBroadcasterThread.register(new FileChangesListener() {
				@Override
				public void onChange(final File f) {


					synchronized (lock) {
						if (lastHeardOf <= f.lastModified() && lastWroteToFile != f.lastModified()) {
							Report.report("There was a local update on the file made by user broadcasting...");
							final Sync sync = new Sync(lastModified = lastHeardOf = f.lastModified());
							//appendID(f);
							lastWroteToFile=f.lastModified();
							broadcast(sync);
						} else {
							Report.report("There was a local update on the file made by the system.");
						}
					}

				}
			});
		}


		if (port>0) {
			server=new AbstractServer<Bundle>(port) {

				@Override
				protected void handleMessageFromClient(final Bundle msg,final ConnectionToClient client) {
					if (!(msg instanceof Bundle)) return;
					synchronized (lock) {
						Report.report("Server of this node Recieved " + msg + " msg.lastModified = "+ Report.format(msg.getLastModified())  + " lastModified = " + Report.format(lastModified) + " lastHeardOf = " + Report.format(lastHeardOf) + " lastWroteToFile = "+ Report.format(lastWroteToFile)  );
						handleMessege(msg,client);
					}
				}
			};
		}
	}


	/**
	 * @param dataFile
	 * @param i
	 * @param parseInt
	 * @throws IOException 
	 */
	public P2PConnection(final File fileToShare, final int port) throws IOException {
		this(fileToShare,null,port,0);
	}


	/**
	 * @param eVENTOS_DB_FILE
	 * @param string
	 * @throws IOException 
	 */
	public P2PConnection(final File fileToShare, final String ip) throws IOException {
		this(fileToShare,ip,0,10);
	}


	protected void handleMessege(final Bundle msg,final Object sendBack) {
		if (msg instanceof Sync) {
			if (msg.getLastModified() >= lastHeardOf && msg.getLastModified() > lastModified) {
				Report.report("Recieved a sync about a new version from some node which this node doesnt have returns sync with lastModified to indicate this node has older file.");
				lastHeardOf=msg.getLastModified();
				sendBack(sendBack,new Sync(lastModified));
			} else if (msg.getLastModified() < lastModified) {
				if (lastModified == lastHeardOf) {
					Report.report("Recieved a sync about some node that holds an older version returns the file.");
					try {
						sendBack(sendBack,new FileData(p2pfileToShare,lastModified));
					} catch (final IOException e) {
						Report.report("couldnt send file");
					}
				}
			} 
		} else if (msg instanceof FileData) {
			if (msg.getLastModified() >= lastHeardOf && msg.getLastModified()>lastModified) {
				try {
					Report.report("Recieved a new version of the file.");
					((FileData) msg).writeToFile(p2pfileToShare);
					lastModified = lastHeardOf = msg.getLastModified();
					lastWroteToFile = p2pfileToShare.lastModified();
					broadcast(new Sync(lastModified));
				} catch (final IOException e) {
					Report.report("locally write failed");
				}
			} 
		} 
	}


	private void sendBack(final Object sendBack, final Bundle sync) {
		new Thread() {
			@Override
			public void run() {
				try {
					if (sendBack instanceof ConnectionToClient) 
						((ConnectionToClient) sendBack).sendToClient(sync);
					else if (sendBack instanceof AbstractClient) 
						((AbstractClient<Bundle>) sendBack).sendToServer(sync);
				} catch (final Exception e) {
					Report.report("couldnt send back bundle to sender, broadcasting to try to recover...");
					broadcast(sync);
				}
			}
		}.start();
	}


	protected void broadcast(final Bundle sync) {
		new Thread() {
			@Override
			public void run() {
				if (server!=null) server.sendToAllClients(sync);
				for (final AbstractClient<Bundle> c : neighborsAccess)
					try {
						c.sendToServer(sync);
					} catch (final IOException e) {
						Report.report("couldnt sync client" + c.getIPPort());
					}
			}
		}.start();

	}


	protected void appendID(final File f) {
		BufferedWriter out = null;
		try {
			out = new BufferedWriter(new FileWriter(f, true));
			out.append("200574929_301540886");
		} catch (final IOException e) {
			Report.report("couldnt print id tags!",e);
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (final IOException ignore) {
				}
			}
		}

	}


	public void start() throws IOException {
		Report.report("Initiailing : ");	
		Report.report("Starting to listen to connections from all (incmoning) neighbors...");	
		if (server!=null) 
			server.listen();
		Report.report("Starting connection handler to make sure connection with all (outgoing) neighbors.");
		if ( !neighborsAccess.isEmpty() ) {
			final Timer connectionThread = new Timer();
			connectionThread.scheduleAtFixedRate(new TimerTask() {
				@Override
				public void run() {
					connectionHandle();
				}

			}, 0, syncTime*1000);
		}

		Report.report("Starting to listen to local changes in file...");	
		if ( fileChangesBroadcasterThread!=null ) fileChangesBroadcasterThread.start();
		Report.report("App is fully initialized, its recommanded that you wait a few seconds to higher the probability that this machine would have the latest file version.");
	}



	public void connectionHandle() {
		for (final AbstractClient<Bundle> c : neighborsAccess) {
			if (c.isConnected()) continue;
			try {
				c.openConnection();
				Report.report("Opened connection with neighbor "+c.getIPPort() + ", Sending sync messege to make sure this machine and neighbor's machine has the latest version of the file.");
				long localLastModified;
				synchronized (lock) {
					localLastModified = lastModified; 
				}
				c.sendToServer(new Sync(localLastModified));

				break;
			} catch (final Exception e) {
				Report.report("Couldnt open connnection to neighbor " + c.getIPPort() + " will try again later");
			}
		}

	}




}
